<?
/**
  * Objectbase PHP Class
  * v.000000002
  * By Harper Reed
  * Harper@nata2.org
  * I am pretty awesome
  * 
  * URL: http://code.google.com/p/nata2/wiki/ObjectbaseClass
  * 
  * I hate documenting my code. I cannot lie. But its pretty easy. 
  * I will make more documentation later. 
  * 
  * This is alpha. Beware
  * 
  * This is version .2. It is very different. It uses the memcache get set style 
  * of caching. it will work with a cluster of memcache servers
  * More documentation when i am done.  
  *
  * -----------------------------------------------------------
  *
  * Copyright 2006-2007 Harper Reed
  * 
  * -----------------------------------------------------------
  *
  * LICENSE
  *
  * This program is free software; you can redistribute it and/or
  * modify it under the terms of the GNU General Public License (GPL)
  * as published by the Free Software Foundation; either version 2
  * of the License, or (at your option) any later version.
  *
  * This program is distributed in the hope that it will be useful,
  * but WITHOUT ANY WARRANTY; without even the implied warranty of
  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  * GNU General Public License for more details.
  *
  * To read the license please visit http://www.gnu.org/copyleft/gpl.html
  *
  */
class objectBase{
  
    public function __construct($options = array()){
        $this->dataDir = "/tmp/";
        $this->dataTempDir = "objectcache";
        
        $this->hash = "objectbase";
        $this->dieOnError = 1;
        $this->debug = 1;
        $this->servers = array(
                array("host"=>'memcache',"port"=>'113211'),
            );
        $this->debugMessage ="";    
        
        if(isset($options['dataDir'])) $this->dataDir = $options['dataDir'];
        if(isset($options['hash'])) $this->hash = $options['hash'];
        if(isset($options['servers'])) $this->servers = $options['servers'];
        if(isset($options['dieOnError'])) $this->dieOnError = $options['dieOnError'];
        if(isset($options['debug'])) $this->debug = $options['debug'];
        
        
        $this->setDebugMessage("testing memcache servers");
        if (count($this->servers)>=1){
            $m = new MemcachedAggregator($this->servers);
            if ($m->status)
            {
                $this->memcache = $m;
                $this->cachingType = 1;
                $this->setDebugMessage("using memcache servers");
            } else $this->cachingType = 0;
        }else $this->cachingType = 0;
        
        if (!($this->cachingType)){
            
            $this->setDebugMessage("testing disk cache");
            $this->dataDir = $this->dataDir.$this->dataTempDir."/";
            
            if(is_dir($this->dataDir)){
                if ($this->is__writable($this->dataDir)){
                    $this->setDebugMessage("using diskcache");
                    $this->cachingType =2;
                }else $this->cachingType = 0;
            }else {
                $this->setDebugMessage($this->dataDir . " is not writable or doesn't exist'");
                if ( mkdir($this->dataDir)){
                        $this->setDebugMessage($this->dataDir . " created and rocking");
                        $this->cachingType =2;    	
                }else $this->cachingType = 0;
            } 
        }
        
        if ($this->cachingType <1)
            $this->setError("Cowardly refusing to cache to nowhere!\r\nPlease ensure that either memcache is running on ".print_r($this->servers,1)."or that your cache dir ".$this->dataDir." is writable");
        
        
    }
    
    private function is__writable($path) {
    //will work in despite of Windows ACLs bug
    //NOTE: use a trailing slash for folders!!!
    //see http://bugs.php.net/bug.php?id=27609
    //see http://bugs.php.net/bug.php?id=30931
    // from http://us2.php.net/manual/en/function.is-writable.php#73596
    
       if ($path{strlen($path)-1}=='/') // recursively return a temporary file path
           return $this->is__writable($path.uniqid(mt_rand()).'.tmp');
       else if (is_dir($path))
           return $this->is__writable($path.'/'.uniqid(mt_rand()).'.tmp');
       // check tmp file for read/write capabilities
       $rm = file_exists($path);
       $f = @fopen($path, 'a');
       if ($f===false)
           return false;
       fclose($f);
       if (!$rm)
           unlink($path);
       return true;
    }
    
    
    private function setError($message){
        $this->errorStatus =1;
        $this->errorMessage = $message;
        $this->setDebugMessage($this->errorMessage);
        if ($this->dieOnError) die($this->errorMessage);
        return 0;
    }
    
    private function hashedKey($_key)
    {
        $hashedkey = md5($this->hash.$_key);
        return $hashedkey;
    }
    
    /**
     * Removes the directory and all its contents.
     *
     * @param string the directory name to remove
     * @param boolean whether to just empty the given directory, without deleting the given directory.
     * @return boolean True/False whether the directory was deleted.
     * 
     * http://us2.php.net/manual/en/function.rmdir.php#72598
     */
    private function deleteDirectory($dirname,$only_empty=false) {
       if (!is_dir($dirname))
           return false;
       $dscan = array(realpath($dirname));
       $darr = array();
       while (!empty($dscan)) {
           $dcur = array_pop($dscan);
           $darr[] = $dcur;
           if ($d=opendir($dcur)) {
               while ($f=readdir($d)) {
                   if ($f=='.' || $f=='..')
                       continue;
                   $f=$dcur.'/'.$f;
                   if (is_dir($f))
                       $dscan[] = $f;
                   else
                       unlink($f);
               }
               closedir($d);
           }
       }
       $i_until = ($only_empty)? 1 : 0;
       for ($i=count($darr)-1; $i>=$i_until; $i--) {
           $msg =  "\nDeleting '".$darr[$i]."' ... ";
           if (rmdir($darr[$i]))
               $msg =  "ok";
           else
                $msg = "FAIL";
               
           $this->setDebugMessage($msg);
       }
       return (($only_empty)? (count(scandir)<=2) : (!is_dir($dirname)));
    }
    
    private function writeFile($_key, $_data){
        
        $objectdir = $this->dataDir.substr($_key,0,5);
        if(!is_dir($objectdir)){
            $this->setDebugMessage($objectdir. " is a new dir");
             mkdir($objectdir);
        }
        
        $filename = $objectdir."/". $_key;
    
        if (!$handle = fopen($filename, 'w')) {
                return  $this->setError("Cannot open file ($filename)");
        }
    
        if (fwrite($handle, $_data) === FALSE) {
            return  $this->setError("Cannot write to file ($filename)");
        }
        
        fclose($handle);
        return 1;
    }
    
    //
    private function getDiskObject($_key){
        $this->setDebugMessage("getting cached object using diskcache");
        $hashedkey = $this->hashedKey($_key);
        $objectdir = $this->dataDir.substr($hashedkey,0,5);
        $filename = $objectdir."/". $hashedkey;
        $this->setDebugMessage("getting cached object from ".$filename);
        if (file_exists($filename)){
            $readcontent = file_get_contents($filename);
            $storedobject = unserialize($readcontent);
            $now = mktime();
            $this->setDebugMessage("cached object is ". (mktime()-$storedobject["startdate"]) . " seconds behind. TTL: ".($storedobject["expire"]-(mktime()-$storedobject["startdate"])));
            if ((mktime()-$storedobject["startdate"])<$storedobject["expire"])
            {
                return $storedobject["data"];	
            }else{
                $this->deleteDiskObject($_key);
                return false;	
            }
        }return false;
    }
    
    private function getMemObject($_key)
    {
    	$hashedkey = $this->hashedKey($_key);
        $this->setDebugMessage("getting cached object using memcache");
        $object = $this->memcache->get($hashedkey);
        return $object;
    }
    
    private function setDiskObject($_key,$_data,$_expire,$_startDate=0)
    {
    	$this->setDebugMessage("caching object using diskcache");
        $hashedkey = $this->hashedKey($_key);
        if ($_startDate==0)
        $startdate = mktime();
        else $startdate=$_startDate;
        $storedobject = array("data"=>$_data, "expire"=>$_expire,"startdate"=>$startdate);
        
        $data = serialize($storedobject);
        if ($this->writefile($hashedkey,$data)){
            $this->setDebugMessage("write to disk cache successful");
        	$success =1;
        }else $success =0; 
        return $success;
    }
    
    private function setMemObject($_key,$_data,$_compress,$_expire)
    {
    	$hashedkey = $this->hashedKey($_key);
        $success = $this->memcache->set($hashedkey, $_data, $_compress, $_expire) or $this->setError("Could not save data to memcache");
        $this->setDebugMessage("caching object using memcache");
        return $success;
    }
    
    private function deleteMemObject($_key)
    {
    	
        $hashedkey = $this->hashedKey($_key);
    	$success = $this->memcache->delete($hashedkey);
        return $success;
    }
    
    private function deleteDiskObject($_key)
    {
    	$hashedkey = $this->hashedKey($_key);
        $objectdir = $this->dataDir.substr($hashedkey,0,5);
        $filename = $objectdir."/". $hashedkey;
        if (file_exists($filename)){
            unlink($filename);
        }else return true;
    }
    
    private function setDebugMessage($_message)
    {
        if ($this->debug)
            $this->debugMessage[] = array("date"=>date("r"),"message"=>$_message);
    }
    
    private function flushMemObjects()
    {
    	$this->setDebugMessage("flushing objects using memcache");
        $success = $this->memcache->flush();
        return $success;
    }
    
    private function flushDiskObjects()
    {
    	
        $this->setDebugMessage("flushing objects using diskcache");
        $success = $this->deleteDirectory($this->dataDir);
        return $success;
    }
    
    private function replaceDiskObject($_key,$_data,$_expire)
    {
        $this->setDebugMessage("replacing ".$_key." object using diskcache");
        $hashedkey = $this->hashedKey($_key);
        $objectdir = $this->dataDir.substr($hashedkey,0,5);
        $filename = $objectdir."/". $hashedkey;
        $this->setDebugMessage("getting cached object from ".$filename);
        if (file_exists($filename)){
            $readcontent = file_get_contents($filename);
            $storedobject = unserialize($readcontent);
            $now = mktime();
            $this->setDebugMessage("cached object is ". (mktime()-$storedobject["startdate"]) . " seconds behind. TTL: ".($storedobject["expire"]-(mktime()-$storedobject["startdate"])));
            if ((mktime()-$storedobject["startdate"])<$storedobject["expire"])
            {
            	
                if ($_expire==0)$_expire = ($storedobject["expire"]-(mktime()-$storedobject["startdate"]));
                $success =$this->setDiskObject($_key,$_data,$_expire,$storedobject["startdate"]);
                return $success;        
            }else{
                $this->deleteDiskObject($_key);
                return false;   
            }
        }
        
        
    }
    
    private function replaceMemObject($_key,$_data,$_compress,$_expire)
    {
        $hashedkey = $this->hashedKey($_key);
        $success = $this->memcache->replace($hashedkey, $_data, $_compress, $_expire) or $this->setError("Could not save data to memcache");
        $this->setDebugMessage("replacing ".$_key." object using memcache");
        return $success;
    }
    
    /**
     * Public functions 
     */
     
     
     
    public function errorHandler(){
        return $this->errorMessage;
    }
    
    public function debugHandler(){
        return $this->debugMessage;
    }
    
    /**
     * set
     * 
     * Note:  Remember that resource variables (i.e. file and connection descriptors) cannot be stored in the cache, because they cannot be adequately represented in serialized state.
     */
    public function set( $_key, $_data, $_compress=0, $_expire= 2592000 )
    {
        $this->setDebugMessage("data:".print_r($_data,1));
        if ($this->cachingType==1){
            $success = $this->setMemObject($_key,$_data,$_compress,$_expire);
        }else if ($this->cachingType==2){
            $success = $this->setDiskObject($_key,$_data, $_expire); // store object
        }
        return $success;
    }
    
    public function get( $_key)
    {
    	if ($this->cachingType==1){
            $object = $this->getMemObject($_key);
        }else if ($this->cachingType==2){
            $object = $this->getDiskObject($_key);
        }
        return $object;
    }

    public function delete( $_key)
    {
    	if ($this->cachingType==1){
            $success = $this->deleteMemObject($_key);
        }else if ($this->cachingType==2){
            $success = $this->deleteDiskObject($_key);
        }
        return $success;
    }
    
    public function flush()
    {
        if ($this->cachingType==1){
            $success = $this->flushMemObjects();
        }else if ($this->cachingType==2){
            $success = $this->flushDiskObjects();
        }
        return $success;
    }

    public function replace( $_key, $_data, $_compress=0, $_expire= 0 )
    {
        $this->setDebugMessage("replace ".$_key." with data:".print_r($_data,1));
        if ($this->cachingType==1){
            $success = $this->replaceMemObject($_key,$_data,$_compress,$_expire);
        }else if ($this->cachingType==2){
            $success = $this->replaceDiskObject($_key,$_data, $_expire); // store object
        }
        return $success;	
    }
}

?>
